What separates a prototype from a real tool is what happens when things go wrong.
Day 36 of 80
Your Prompt Vault currently works — as long as everything goes right. But software in the real world encounters problems constantly. The API times out. A file gets corrupted. A user submits a blank form.
The difference between a prototype and a tool you actually trust is how the app behaves when things go sideways. A prototype crashes or shows a raw Python traceback. A polished tool shows a clear message and stays running.
Today you add that layer of resilience. It's not glamorous work — but it's what makes the app professional.
Your Prompt Vault has four main places where things can fail. Each one needs a different approach.
| # | Scenario | What happens without handling | What you'll do |
|---|---|---|---|
| 1 | Claude API is down or rate-limited | 500 error, ugly traceback in browser | Catch the exception, show a friendly message |
| 2 | prompts.json is corrupted JSON | App won't start, json.JSONDecodeError crash |
Catch decode error, log it, start with empty list |
| 3 | User submits empty form fields | Prompt is generated with blank inputs — garbage output | Validate before sending to Claude |
| 4 | Invalid platform name in JSON | Jinja template errors, broken cards | Validate platform on load, skip invalid entries |
Anthropic's SDK raises specific exception types depending on what went wrong. You can catch each one separately and give the user a useful message.
# At the top of vault.py, import anthropic's error types
import anthropic
# Inside your /generate route, replace the bare .create() call with this:
def generate_prompt():
error = None
generated = None
try:
message = claude.messages.create(
model="claude-opus-4-5",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
generated = message.content[0].text
except anthropic.RateLimitError:
error = "Too many requests — wait a minute and try again."
except anthropic.AuthenticationError:
error = "API key problem — check your .env file."
except anthropic.APIStatusError as e:
error = f"API error: {e.status_code} — {e.message}"
except anthropic.APIConnectionError:
error = "Could not reach Claude. Check your internet connection."
except Exception as e:
error = f"Something went wrong: {e}"
if error:
return render_template("index.html", prompts=prompts, error=error)
# Only reach here if generation succeeded
return render_template("index.html", prompts=prompts, generated=generated)
Why separate exception types? A RateLimitError means "slow down" — the user can retry in a minute. An AuthenticationError means the key is wrong — retrying won't help. Catching them separately lets you give advice that's actually useful.
The catch-all except Exception is a safety net. It catches anything you didn't anticipate. Log these when you see them — they're bugs to fix later.
The error variable pattern keeps the logic readable. Start with None, set it only on failure, check it at the end. This is cleaner than nested if/else blocks.
If prompts.json gets corrupted — maybe it was partially written when the process was killed — your app will crash on startup. Fix it in helpers.py.
import json
PROMPTS_FILE = "prompts.json"
def load_prompts():
try:
with open(PROMPTS_FILE, "r") as f:
return json.load(f)
except FileNotFoundError:
# First run — no file yet. That's fine, return empty list.
return []
except json.JSONDecodeError:
# File exists but is corrupted. Log a warning, start fresh.
print(f"WARNING: {PROMPTS_FILE} is corrupted — starting with empty library")
return []
def save_prompts(prompts):
try:
with open(PROMPTS_FILE, "w") as f:
json.dump(prompts, f, indent=2)
except IOError as e:
print(f"ERROR: Could not save prompts: {e}")
Two different failures, two different meanings: FileNotFoundError is normal on a fresh install. JSONDecodeError means something went wrong — the print statement leaves a trace in your terminal so you know it happened.
Save also needs protection. An IOError on save could happen if the disk is full or permissions are wrong. Catching it means the app keeps running even if it can't persist data.
Before you even call Claude, check that the user filled in the form. This is a Flask route concern — do it at the top of your /generate route.
# At the start of the POST handler in /generate:
platform = request.form.get("platform", "").strip()
shot = request.form.get("shot", "").strip()
VALID_PLATFORMS = ["Kling", "Runway", "Veo"]
if not platform or not shot:
error = "Please fill in both Platform and Shot Description."
return render_template("index.html", prompts=load_prompts(), error=error)
if platform not in VALID_PLATFORMS:
error = f"{platform!r} is not a supported platform. Choose from: {', '.join(VALID_PLATFORMS)}"
return render_template("index.html", prompts=load_prompts(), error=error)
# If we get here, inputs are valid — proceed to Claude API call
.get("field", "").strip() is defensive: it handles missing fields, None values, and whitespace-only inputs all at once.
Validate early, return early. Checking inputs before calling Claude avoids wasting API credits on garbage requests. It also makes the control flow easier to follow.
All routes pass an error variable to the template. Now add the display logic in index.html.
{# Add this near the top of your form section #}
{% if error %}
<div class="error-banner">
<span class="error-icon">⚠</span>
{{ error }}
</div>
{% endif %}
{# And add CSS for it in your stylesheet or a <style> block: #}
/*
.error-banner {
background: rgba(239, 68, 68, 0.15);
border: 1px solid rgba(239, 68, 68, 0.4);
border-radius: 8px;
color: #fca5a5;
padding: 12px 16px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
*/
The {% if error %} guard means the banner only appears when there's actually an error. On normal page loads, error is not passed to the template (or is None), so no banner appears.
Styling tip: Use a red with low opacity for the background — it signals danger without being aggressive. The border makes it stand out even more clearly.
Don't assume your error handling works — prove it. To test the rate limit handler, temporarily change your API key to something invalid. To test JSON corruption, open prompts.json in a text editor and delete a closing brace. Reload the app and confirm you see the right message, not a crash.
except Exception fallback is in place as a safety netload_prompts() function handles both FileNotFoundError and JSONDecodeErrorDay 37 covers frontend polish — loading states, responsive layout, character counts, and the empty state design. The app already works; tomorrow it starts to feel professional.